package db

import (
	"fmt"
	"github.com/beego/beego/v2/core/logs"
	_ "github.com/beego/beego/v2/core/logs"
	"github.com/couchbase/gocb/v2"
	"log"
	"reflect"
	"strings"
	"time"
)

type CbCrud[T any] struct {
}

var Bucket *gocb.Bucket
var Conn *gocb.Cluster
var bucketName string

func Connect(host string, username string, password string, bucketN string) {
	bucketName = bucketN
	cluster, err := gocb.Connect("couchbase://"+host, gocb.ClusterOptions{
		Authenticator: gocb.PasswordAuthenticator{
			Username: username,
			Password: password,
		},
	})

	if err != nil {
		log.Fatalf("init CbDb Failed %v", err)
	}
	err = cluster.WaitUntilReady(5*time.Second, nil)
	if err != nil {
		log.Fatalf("init CbDb Failed %v", err)
	}
	Conn = cluster
	Bucket = cluster.Bucket(bucketName)
	log.Printf("init cbDb ok %v", Conn)
}

//func (c *CbCrud[T]) List(fields []string, filter []any, opts *CrudOpts) []T {
//	c.SetTOpts(opts)
//	var list []T
//	pageSize := 200
//	fieldsSql := c.BuildSelect(fields, opts)
//	filterParams := c.BuildFilter(filter, opts)
//	tableName := GetTableName(opts)
//	pageStr := BuildPageStr(0, pageSize)
//	sql := fmt.Sprintf("SELECT %s FROM %s %s %s", fieldsSql, tableName, filterParams.Sql, pageStr)
//	rows := Run(sql, &filterParams, opts)
//	for rows.Next() {
//		var t T
//		_ = rows.Row(&t)
//		list = append(list, t)
//	}
//	return list
//}

func (c *CbCrud[T]) Transaction(fun func(tx *gocb.TransactionAttemptContext) any, opts *CrudOpts) any {
	var reply any
	_, err := Conn.Transactions().Run(func(tx *gocb.TransactionAttemptContext) error {
		c.SetTOpts(opts)
		opts.Tx = tx
		reply = fun(tx)
		return opts.Err
	}, opts.TxOpts)
	if err != nil {
		panic(fmt.Errorf("transaction failed %v", err))
	}
	return reply
}

func (c *CbCrud[T]) Update(sets [][]any, filter []any, opts *CrudOpts) {
	c.SetTOpts(opts)
	setSql := BuildSet(sets, opts)
	tableName := GetTableName(opts)
	obj := opts.Collection
	filterParams := BuildFilter[T](obj, filter, opts)
	sql := fmt.Sprintf("UPDATE %s SET %s WHERE %s", tableName, setSql.Sql, filterParams.Sql)
	c.GetOne(sql, filterParams, opts)
}

func (c *CbCrud[T]) Inserts(datas []T, opts *CrudOpts) {
	filterParams := c.BuildInserts(datas, opts)
	c.GetOne(filterParams.Sql, filterParams, opts)
}

func (c *CbCrud[T]) Insert(data T, opts *CrudOpts) {
	c.Inserts([]T{data}, opts)
}

func (c *CbCrud[T]) Read(fields []string, filter []any, opts *CrudOpts) []T {
	c.SetTOpts(opts)
	obj := opts.Collection
	filterParams := BuildFilter[T](obj, filter, opts)
	tableName := GetTableName(opts)
	fieldStr := BuildSelect(obj, fields, filterParams, opts)
	sql := fmt.Sprintf("SELECT %s FROM %s", fieldStr, tableName)
	sql += filterParams.Sql
	rows := c.GetAll(sql, filterParams, opts)
	return rows
}

func (c *CbCrud[T]) Get(fields []string, filter []any, opts *CrudOpts) T {
	list := c.Read(fields, filter, opts)
	var t T
	if len(list) == 0 {
		return t
	}
	return list[0]
}

func (c *CbCrud[T]) Delete(filter []any, opts *CrudOpts) {
	c.SetTOpts(opts)
	tableName := GetTableName(opts)
	obj := opts.Collection
	filterParams := BuildFilter[T](obj, filter, opts)
	sql := fmt.Sprintf("DELETE FROM %s %s", tableName, filterParams.Sql)
	Run(sql, filterParams, opts)
}

func (c *CbCrud[T]) GetAll(sql string, filterParams *FilterParams, opts *CrudOpts) []T {
	logs.Debug("sql: %s, params: %v", sql, filterParams.Params)
	var list []T
	ctx := opts.Tx
	if ctx == nil {
		rows, err := Conn.Query(sql, &gocb.QueryOptions{PositionalParameters: filterParams.Params, Adhoc: true})
		if err != nil {
			panic(fmt.Errorf("run sql failed, err: %v, sql: %s, params: %v", err, sql, filterParams.Params))
		}
		for rows.Next() {
			var t T
			_ = rows.Row(&t)
			list = append(list, t)
		}
		return list
	}
	rows, err := ctx.Query(sql, &gocb.TransactionQueryOptions{PositionalParameters: filterParams.Params})
	if err != nil {
		panic(fmt.Errorf("run sql failed, err: %v, sql: %s, params: %v", err, sql, filterParams.Params))
	}
	for rows.Next() {
		var t T
		err := rows.Row(&t)
		if err != nil {
			logs.Info("get row failed, err: %v", err)
		}
		list = append(list, t)
	}
	return list
}

func (c *CbCrud[T]) GetOne(sql string, filterParams *FilterParams, opts *CrudOpts) T {
	logs.Debug("sql: %s, params: %v", sql, filterParams.Params)
	var t T
	tx := opts.Tx
	if tx == nil {
		rows, err := Conn.Query(sql, &gocb.QueryOptions{PositionalParameters: filterParams.Params, Adhoc: true})
		if err != nil {
			panic(fmt.Errorf("run query failed: %v, sql: %s, params: %v", err, sql, filterParams.Params))
		}
		if rows.Next() {
			_ = rows.One(&t)
		}
		return t
	}
	rows, err := tx.Query(sql, &gocb.TransactionQueryOptions{PositionalParameters: filterParams.Params})
	if err != nil {
		panic(fmt.Errorf("run query failed: %v, sql: %s, params: %v", err, sql, filterParams.Params))
	}
	if rows.Next() {
		_ = rows.One(&t)
	}
	return t
}

func Run(sql string, filterParams *FilterParams, opts *CrudOpts) *gocb.QueryResult {
	logs.Debug("%s, params: %v", sql, filterParams.Params)
	rows, err := Conn.Query(sql, &gocb.QueryOptions{PositionalParameters: filterParams.Params, Adhoc: true})
	if err != nil {
		panic(fmt.Errorf("run query failed: %v, sql: %s, params: %v", err, sql, filterParams.Params))
	}
	return rows
}

func (c *CbCrud[T]) SetTOpts(opts *CrudOpts) {
	if opts.Collection != "" && opts.JsonPK != "" && opts.PrimaryKey != "" {
		return
	}
	var t T
	a := reflect.TypeOf(t)
	f := a.Field(0)
	method, b := a.MethodByName("GetTableName")
	if !b {
		panic(fmt.Errorf("%v not found method GetTableName", a))
	}
	args := opts.Args
	if args == nil {
		args = map[string]any{}
	}
	mA := []reflect.Value{reflect.ValueOf(t), reflect.ValueOf(args), reflect.ValueOf(opts)}
	tableName := method.Func.Call(mA)
	opts.Collection = tableName[0].String()
	opts.JsonPK = strings.Split(f.Tag.Get("json"), ",")[0]
	opts.PrimaryKey = f.Name
}

func (c *CbCrud[T]) BuildInserts(datas []T, opts *CrudOpts) *FilterParams {
	c.SetTOpts(opts)
	pK := opts.PrimaryKey
	tableName := GetTableName(opts)
	params := make([]any, 2*len(datas))
	i := 0
	sql := ""
	for _, data := range datas {
		if i > 0 {
			sql += ","
		}
		sql += fmt.Sprintf("($%d,$%d)", i+1, i+2)
		params[i] = reflect.ValueOf(data).FieldByName(pK).String()
		//d, _ := json.Marshal(data)
		params[i+1] = data
		i = i + 2
	}
	sql = "INSERT INTO " + tableName + "(KEY, VALUE) VALUES" + sql
	return &FilterParams{Sql: sql, Params: params, Index: i}
}

func (c *CbCrud[T]) BuildInsert(datas T, opts *CrudOpts) *FilterParams {
	return c.BuildInserts([]T{datas}, opts)
}

//func (c *CbCrud[T]) BuildSelect(fields []string, opts *CrudOpts) string {
//	if len(fields) == 0 {
//		return "*"
//	}
//	var d T
//	t := reflect.TypeOf(d)
//	sql := ""
//	for _, field := range fields {
//		ffs, err := t.FieldByName(field)
//		if err == true {
//			field = ffs.Tag.Get("json")
//		}
//		if sql == "" {
//			sql = field
//		} else {
//			sql += fmt.Sprintf(",%s", field)
//		}
//	}
//	return sql
//}

func BuildSet(sets [][]any, opts *CrudOpts) *FilterParams {
	sql := ""
	if len(sets) == 0 {
		panic(fmt.Errorf("set_empty"))
	}
	oLen := len(opts.Params)
	params := make([]any, oLen+len(sets)-1)
	i := oLen
	for index, set := range sets {
		if index > 0 {
			sql += ","
		}
		sql = fmt.Sprintf("%s = $%d", set[0], i)
		params = append(params, set[1])
		i++
	}
	opts.Params = append(opts.Params, params...)
	return &FilterParams{Sql: sql, Params: params}
}

func GetTableName(opts *CrudOpts) string {
	collection := opts.Collection
	scope := opts.Scope
	if scope == "" {
		panic(fmt.Errorf("scope_empty"))
	}
	if collection != "" {
		return bucketName + "." + scope + "." + collection
	} else {
		return bucketName + "." + scope
	}
}

func BuildPageStr(page, pageSize int) string {
	if page < 1 && pageSize < 20 {
		logs.Debug("page: %d Error", page)
		panic(fmt.Sprintf("page_error"))
	}
	return fmt.Sprintf("\nLIMIT %d OFFSET %d", pageSize, (page-1)*pageSize)
}

func OperNotValid(op string) bool {
	return op != "anyIn" && op != ">" && op != "<" && op != ">=" && op != "<=" && op != "!=" && op != "LIKE"
}
